package edu.northwestern.cbits.purple_robot_manager.plugins;
import java.io.File;
import java.math.BigInteger;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLPeerUnverifiedException;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.support.v7.app.NotificationCompat;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.MultipartBuilder;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import edu.northwestern.cbits.purple_robot_manager.EncryptionManager;
import edu.northwestern.cbits.purple_robot_manager.PowerHelper;
import edu.northwestern.cbits.purple_robot_manager.R;
import edu.northwestern.cbits.purple_robot_manager.WiFiHelper;
import edu.northwestern.cbits.purple_robot_manager.activities.StartActivity;
import edu.northwestern.cbits.purple_robot_manager.logging.LiberalSSLSocketFactory;
import edu.northwestern.cbits.purple_robot_manager.logging.LogManager;
import edu.northwestern.cbits.purple_robot_manager.logging.SanityManager;
import edu.northwestern.cbits.purple_robot_manager.probes.Probe;
@SuppressLint("NewApi")
public abstract class DataUploadPlugin extends OutputPlugin
{
public final static String USER_HASH_KEY = "UserHash";
public final static String OPERATION_KEY = "Operation";
public final static String PAYLOAD_KEY = "Payload";
public final static String CHECKSUM_KEY = "Checksum";
public final static String CONTENT_LENGTH_KEY = "ContentLength";
public final static String STATUS_KEY = "Status";
private final static String CACHE_DIR = "pending_uploads";
public static final String TRANSMIT_KEY = "TRANSMIT";
public static final String RESTRICT_TO_WIFI = "config_restrict_data_wifi";
private static final boolean RESTRICT_TO_WIFI_DEFAULT = true;
public static final String UPLOAD_URI = "config_data_server_uri";
private static final String RESTRICT_TO_CHARGING = "config_restrict_data_charging";
private static final boolean RESTRICT_TO_CHARGING_DEFAULT = false;
public static final String ALLOW_ALL_SSL_CERTIFICATES = "config_http_liberal_ssl";
public static final boolean ALLOW_ALL_SSL_CERTIFICATES_DEFAULT = true;
protected static final int RESULT_SUCCESS = 0;
protected static final int RESULT_NO_CONNECTION = 1;
protected static final int RESULT_ERROR = 2;
protected static final int RESULT_NO_POWER = 3;
public File getPendingFolder()
{
SharedPreferences prefs = HttpUploadPlugin.getPreferences(this.getContext());
File internalStorage = this.getContext().getFilesDir();
if (prefs.getBoolean(OutputPlugin.USE_EXTERNAL_STORAGE, false))
internalStorage = this.getContext().getExternalFilesDir(null);
if (internalStorage != null && !internalStorage.exists())
internalStorage.mkdirs();
File pendingFolder = new File(internalStorage, DataUploadPlugin.CACHE_DIR);
if (pendingFolder != null && !pendingFolder.exists())
pendingFolder.mkdirs();
return pendingFolder;
}
public static boolean restrictToWifi(SharedPreferences prefs)
{
try
{
return prefs.getBoolean(DataUploadPlugin.RESTRICT_TO_WIFI, DataUploadPlugin.RESTRICT_TO_WIFI_DEFAULT);
}
catch (ClassCastException e)
{
String enabled = prefs.getString(DataUploadPlugin.RESTRICT_TO_WIFI,
"" + DataUploadPlugin.RESTRICT_TO_WIFI_DEFAULT).toLowerCase(Locale.ENGLISH);
boolean isRestricted = ("false".equals(enabled) == false);
Editor edit = prefs.edit();
edit.putBoolean(DataUploadPlugin.RESTRICT_TO_WIFI, isRestricted);
edit.commit();
return isRestricted;
}
}
public static boolean restrictToCharging(SharedPreferences prefs)
{
try
{
return prefs.getBoolean(DataUploadPlugin.RESTRICT_TO_CHARGING, DataUploadPlugin.RESTRICT_TO_CHARGING_DEFAULT);
}
catch (ClassCastException e)
{
String enabled = prefs.getString(DataUploadPlugin.RESTRICT_TO_CHARGING,
"" + DataUploadPlugin.RESTRICT_TO_CHARGING_DEFAULT).toLowerCase(Locale.ENGLISH);
boolean isRestricted = ("false".equals(enabled) == false);
Editor edit = prefs.edit();
edit.putBoolean(DataUploadPlugin.RESTRICT_TO_CHARGING, isRestricted);
edit.commit();
return isRestricted;
}
}
public boolean shouldAttemptUpload(Context context)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final DataUploadPlugin me = this;
if (DataUploadPlugin.restrictToWifi(prefs))
{
if (WiFiHelper.wifiAvailable(context) == false)
{
me.broadcastMessage(context.getString(R.string.message_wifi_pending), false);
return false;
}
}
if (DataUploadPlugin.restrictToCharging(prefs))
{
if (PowerHelper.isPluggedIn(context) == false)
{
me.broadcastMessage(context.getString(R.string.message_charging_pending), false);
return false;
}
}
return true;
}
@SuppressWarnings("deprecation")
protected int transmitPayload(SharedPreferences prefs, String payload)
{
Context context = this.getContext();
if (prefs == null)
prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (payload == null || payload.trim().length() == 0)
{
LogManager.getInstance(context).log("null_or_empty_payload", null);
return DataUploadPlugin.RESULT_SUCCESS;
}
final DataUploadPlugin me = this;
try
{
try
{
if (DataUploadPlugin.restrictToWifi(prefs))
{
if (WiFiHelper.wifiAvailable(context) == false)
{
me.broadcastMessage(context.getString(R.string.message_wifi_pending), false);
return DataUploadPlugin.RESULT_NO_CONNECTION;
}
}
if (DataUploadPlugin.restrictToCharging(prefs))
{
if (PowerHelper.isPluggedIn(context) == false)
{
me.broadcastMessage(context.getString(R.string.message_charging_pending), false);
return DataUploadPlugin.RESULT_NO_POWER;
}
}
JSONObject jsonMessage = new JSONObject();
jsonMessage.put(OPERATION_KEY, "SubmitProbes");
payload = payload.replaceAll("\r", "");
payload = payload.replaceAll("\n", "");
jsonMessage.put(PAYLOAD_KEY, payload);
String userHash = EncryptionManager.getInstance().getUserHash(me.getContext());
jsonMessage.put(USER_HASH_KEY, userHash);
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] checksummed = (jsonMessage.get(USER_HASH_KEY).toString()
+ jsonMessage.get(OPERATION_KEY).toString() + jsonMessage.get(PAYLOAD_KEY).toString())
.getBytes("UTF-8");
byte[] digest = md.digest(checksummed);
String checksum = (new BigInteger(1, digest)).toString(16);
while (checksum.length() < 32)
checksum = "0" + checksum;
jsonMessage.put(CHECKSUM_KEY, checksum);
jsonMessage.put(CONTENT_LENGTH_KEY, checksummed.length);
// Liberal HTTPS setup:
// http://stackoverflow.com/questions/2012497/accepting-a-certificate-for-https-on-android
HostnameVerifier hostnameVerifier = org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER;
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
SSLSocketFactory socketFactory = SSLSocketFactory.getSocketFactory();
if (prefs.getBoolean(DataUploadPlugin.ALLOW_ALL_SSL_CERTIFICATES,
DataUploadPlugin.ALLOW_ALL_SSL_CERTIFICATES_DEFAULT))
{
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
socketFactory = new LiberalSSLSocketFactory(trustStore);
}
registry.register(new Scheme("https", socketFactory, 443));
OkHttpClient client = new OkHttpClient();
client.setConnectTimeout(3, TimeUnit.MINUTES);
client.setReadTimeout(3, TimeUnit.MINUTES);
String title = me.getContext().getString(R.string.notify_upload_data);
PendingIntent contentIntent = PendingIntent.getActivity(me.getContext(), 0, new Intent(me.getContext(), StartActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder noteBuilder = new NotificationCompat.Builder(me.getContext());
noteBuilder.setContentTitle(title);
noteBuilder.setContentText(title);
noteBuilder.setContentIntent(contentIntent);
noteBuilder.setSmallIcon(R.drawable.ic_note_normal);
noteBuilder.setWhen(System.currentTimeMillis());
noteBuilder.setColor(0xff4e015c);
Notification note = noteBuilder.build();
note.flags = Notification.FLAG_ONGOING_EVENT;
String uriString = prefs.getString(DataUploadPlugin.UPLOAD_URI, context.getString(R.string.sensor_upload_url));
String jsonString = jsonMessage.toString();
String uploadMessage = String.format(context.getString(R.string.message_transmit_bytes), (jsonString.length() / 1024));
me.broadcastMessage(uploadMessage, false);
MultipartBuilder builder = new MultipartBuilder();
builder = builder.type(MultipartBuilder.FORM);
ArrayList<File> toDelete = new ArrayList<>();
if (payload.contains("\"" + Probe.PROBE_MEDIA_URL + "\":"))
{
JSONArray payloadJson = new JSONArray(payload);
for (int i = 0; i < payloadJson.length(); i++)
{
JSONObject reading = payloadJson.getJSONObject(i);
if (reading.has(Probe.PROBE_MEDIA_URL))
{
Uri u = Uri.parse(reading.getString(Probe.PROBE_MEDIA_URL));
String mimeType = "application/octet-stream";
if (reading.has(Probe.PROBE_MEDIA_CONTENT_TYPE))
mimeType = reading.getString(Probe.PROBE_MEDIA_CONTENT_TYPE);
String guid = reading.getString(Probe.PROBE_GUID);
File file = new File(u.getPath());
if (file.exists()) {
builder = builder.addFormDataPart(guid, file.getName(), RequestBody.create(MediaType.parse(mimeType), file));
toDelete.add(file);
}
}
}
}
builder = builder.addPart(Headers.of("Content-Disposition", "form-data; name=\"json\""), RequestBody.create(null, jsonString));
String version = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
RequestBody requestBody = builder.build();
Request request = new Request.Builder()
.removeHeader("User-Agent")
.addHeader("User-Agent", "Purple Robot " + version)
.url(uriString)
.post(requestBody)
.build();
Response response = client.newCall(request).execute();
JSONObject json = new JSONObject(response.body().string());
String status = json.getString(STATUS_KEY);
String responsePayload = "";
if (json.has(PAYLOAD_KEY))
responsePayload = json.getString(PAYLOAD_KEY);
if (status.equals("error") == false)
{
byte[] responseDigest = md.digest((status + responsePayload).getBytes("UTF-8"));
String responseChecksum = (new BigInteger(1, responseDigest)).toString(16);
while (responseChecksum.length() < 32)
responseChecksum = "0" + responseChecksum;
if (responseChecksum.equals(json.getString(CHECKSUM_KEY)))
{
String uploadedMessage = String.format(context.getString(R.string.message_upload_successful),
(jsonString.length() / 1024));
me.broadcastMessage(uploadedMessage, false);
for (File f : toDelete)
f.delete();
return DataUploadPlugin.RESULT_SUCCESS;
}
else
{
HashMap<String, Object> logPayload = new HashMap<>();
logPayload.put("remote_checksum", json.getString(CHECKSUM_KEY));
logPayload.put("local_checksum", responseChecksum);
LogManager.getInstance(context).log("null_or_empty_payload", logPayload);
me.broadcastMessage(context.getString(R.string.message_checksum_failed), true);
}
return DataUploadPlugin.RESULT_ERROR;
}
else
{
String errorMessage = String.format(context.getString(R.string.message_server_error), status);
me.broadcastMessage(errorMessage, true);
}
}
catch (HttpHostConnectException | ConnectTimeoutException e)
{
e.printStackTrace();
me.broadcastMessage(context.getString(R.string.message_http_connection_error), true);
LogManager.getInstance(context).logException(e);
} catch (SocketTimeoutException e)
{
e.printStackTrace();
me.broadcastMessage(context.getString(R.string.message_socket_timeout_error), true);
LogManager.getInstance(me.getContext()).logException(e);
}
catch (SocketException e)
{
e.printStackTrace();
String errorMessage = String.format(context.getString(R.string.message_socket_error), e.getMessage());
me.broadcastMessage(errorMessage, true);
LogManager.getInstance(me.getContext()).logException(e);
}
catch (UnknownHostException e)
{
e.printStackTrace();
me.broadcastMessage(context.getString(R.string.message_unreachable_error), true);
LogManager.getInstance(me.getContext()).logException(e);
}
catch (JSONException e)
{
e.printStackTrace();
me.broadcastMessage(context.getString(R.string.message_response_error), true);
LogManager.getInstance(me.getContext()).logException(e);
}
catch (SSLPeerUnverifiedException e)
{
LogManager.getInstance(me.getContext()).logException(e);
me.broadcastMessage(context.getString(R.string.message_unverified_server), true);
LogManager.getInstance(context).logException(e);
}
catch (Exception e)
{
e.printStackTrace();
LogManager.getInstance(me.getContext()).logException(e);
me.broadcastMessage(context.getString(R.string.message_general_error, e.getMessage()), true);
LogManager.getInstance(context).logException(e);
}
}
catch (OutOfMemoryError e)
{
LogManager.getInstance(me.getContext()).logException(e);
me.broadcastMessage(context.getString(R.string.message_general_error, e.getMessage()), true);
LogManager.getInstance(context).logException(e);
}
return DataUploadPlugin.RESULT_ERROR;
}
public abstract boolean isEnabled(Context context);
public static boolean uploadEnabled(Context context)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (prefs.getBoolean(HttpUploadPlugin.ENABLED, HttpUploadPlugin.ENABLED_DEFAULT))
return true;
else if (prefs.getBoolean(StreamingJacksonUploadPlugin.ENABLED, StreamingJacksonUploadPlugin.ENABLED_DEFAULT))
return true;
return false;
}
public static long lastUploadTime(SharedPreferences prefs)
{
long lastUpload = -1;
if (prefs.contains(HttpUploadPlugin.LAST_UPLOAD_TIME))
lastUpload = prefs.getLong(HttpUploadPlugin.LAST_UPLOAD_TIME, -1);
if (prefs.contains(StreamingJacksonUploadPlugin.LAST_UPLOAD_TIME))
{
long streamingUpload = prefs.getLong(StreamingJacksonUploadPlugin.LAST_UPLOAD_TIME, -1);
if (streamingUpload > lastUpload)
lastUpload = streamingUpload;
}
return lastUpload;
}
public static long lastUploadSize(SharedPreferences prefs)
{
long lastSize = -1;
if (prefs.contains(HttpUploadPlugin.LAST_UPLOAD_SIZE))
lastSize = prefs.getLong(HttpUploadPlugin.LAST_UPLOAD_SIZE, -1);
if (prefs.contains(StreamingJacksonUploadPlugin.LAST_UPLOAD_SIZE))
{
long lastUpload = -1;
if (prefs.contains(HttpUploadPlugin.LAST_UPLOAD_TIME))
lastUpload = prefs.getLong(HttpUploadPlugin.LAST_UPLOAD_TIME, -1);
if (prefs.contains(StreamingJacksonUploadPlugin.LAST_UPLOAD_TIME))
{
long streamingUpload = prefs.getLong(StreamingJacksonUploadPlugin.LAST_UPLOAD_TIME, -1);
if (streamingUpload > lastUpload)
lastSize = prefs.getLong(StreamingJacksonUploadPlugin.LAST_UPLOAD_SIZE, -1);
}
}
return lastSize;
}
public static int pendingFileCount(Context context)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (prefs.getBoolean(HttpUploadPlugin.ENABLED, HttpUploadPlugin.ENABLED_DEFAULT))
{
OutputPlugin plugin = OutputPluginManager.sharedInstance.pluginForClass(context, HttpUploadPlugin.class);
if (plugin instanceof HttpUploadPlugin)
{
HttpUploadPlugin http = (HttpUploadPlugin) plugin;
return http.pendingFilesCount();
}
}
else
{
OutputPlugin plugin = OutputPluginManager.sharedInstance.pluginForClass(context, StreamingJacksonUploadPlugin.class);
if (plugin instanceof StreamingJacksonUploadPlugin)
{
StreamingJacksonUploadPlugin http = (StreamingJacksonUploadPlugin) plugin;
return http.pendingFilesCount();
}
}
return 0;
}
public static boolean multipleUploadersEnabled(Context context)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (prefs.getBoolean(HttpUploadPlugin.ENABLED, HttpUploadPlugin.ENABLED_DEFAULT) &&
prefs.getBoolean(StreamingJacksonUploadPlugin.ENABLED, StreamingJacksonUploadPlugin.ENABLED_DEFAULT))
{
return true;
}
return false;
}
}